• ABOUT
  • 2017
  • 2018
  • 2019
  • 2020
  • 2021
  • COMPARAISON

Load all necessary libraries¶

In [88]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import squarify
import folium
import json
import seaborn as sbn
from urllib.request import urlopen
import plotly.express as px
import warnings
warnings.filterwarnings('ignore')

Import data¶

In [89]:
fullData = pd.read_csv('valeursfoncieres-2019.txt', sep='|')

fullData2017 = pd.read_csv('valeursfoncieres-2017.txt', sep='|')
fullData2018 = pd.read_csv('valeursfoncieres-2018.txt', sep='|')
fullData2019 = pd.read_csv('valeursfoncieres-2019.txt', sep='|')
fullData2020 = pd.read_csv('valeursfoncieres-2020.txt', sep='|')
fullData2021 = pd.read_csv('valeursfoncieres-2021.txt', sep='|')
In [90]:
fullData
Out[90]:
Code service CH Reference document 1 Articles CGI 2 Articles CGI 3 Articles CGI 4 Articles CGI 5 Articles CGI No disposition Date mutation Nature mutation ... Surface Carrez du 5eme lot Nombre de lots Code type local Type local Identifiant local Surface reelle bati Nombre pieces principales Nature culture Nature culture speciale Surface terrain
0 NaN NaN NaN NaN NaN NaN NaN 1 04/01/2019 Vente ... NaN 1 2.0 Appartement NaN 20.0 1.0 NaN NaN NaN
1 NaN NaN NaN NaN NaN NaN NaN 1 04/01/2019 Vente ... NaN 1 3.0 Dépendance NaN 0.0 0.0 NaN NaN NaN
2 NaN NaN NaN NaN NaN NaN NaN 1 04/01/2019 Vente ... NaN 2 2.0 Appartement NaN 62.0 3.0 NaN NaN NaN
3 NaN NaN NaN NaN NaN NaN NaN 1 08/01/2019 Vente ... NaN 0 1.0 Maison NaN 90.0 4.0 S NaN 940.0
4 NaN NaN NaN NaN NaN NaN NaN 1 07/01/2019 Vente ... NaN 0 1.0 Maison NaN 101.0 5.0 S NaN 490.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
3618974 NaN NaN NaN NaN NaN NaN NaN 1 30/12/2019 Vente ... NaN 3 4.0 Local industriel. commercial ou assimilé NaN 100.0 0.0 NaN NaN NaN
3618975 NaN NaN NaN NaN NaN NaN NaN 1 17/12/2019 Adjudication ... NaN 2 2.0 Appartement NaN 45.0 2.0 NaN NaN NaN
3618976 NaN NaN NaN NaN NaN NaN NaN 1 05/12/2019 Vente ... NaN 2 4.0 Local industriel. commercial ou assimilé NaN 47.0 0.0 NaN NaN NaN
3618977 NaN NaN NaN NaN NaN NaN NaN 1 12/12/2019 Adjudication ... NaN 1 3.0 Dépendance NaN 0.0 0.0 NaN NaN NaN
3618978 NaN NaN NaN NaN NaN NaN NaN 1 26/12/2019 Vente ... NaN 1 2.0 Appartement NaN 4.0 1.0 NaN NaN NaN

3618979 rows × 43 columns

Clean data¶

In [91]:
columns_to_keep = ['Date mutation','Nature mutation','Valeur fonciere','Code postal','Commune','Code departement','Code commune','Nombre de lots','Code type local','Type local','Surface reelle bati','Nombre pieces principales','Surface terrain']
fullData['Date mutation'] = pd.to_datetime(fullData['Date mutation'])
fullData['Code departement'] = fullData['Code departement'].astype(str)
fullData = fullData[columns_to_keep]
fullData = fullData.dropna()
fullData['Valeur fonciere'] = pd.to_numeric(fullData['Valeur fonciere'].str.replace(',', '.'))
fullData
Out[91]:
Date mutation Nature mutation Valeur fonciere Code postal Commune Code departement Code commune Nombre de lots Code type local Type local Surface reelle bati Nombre pieces principales Surface terrain
3 2019-08-01 Vente 209000.0 1160.0 PRIAY 1 314 0 1.0 Maison 90.0 4.0 940.0
4 2019-07-01 Vente 134900.0 1370.0 SAINT-ETIENNE-DU-BOIS 1 350 0 1.0 Maison 101.0 5.0 490.0
5 2019-03-01 Vente 192000.0 1340.0 ATTIGNAT 1 24 0 1.0 Maison 88.0 4.0 708.0
6 2019-08-01 Vente 45000.0 1250.0 CIZE 1 106 0 1.0 Maison 39.0 2.0 631.0
13 2019-07-01 Vente 116000.0 1560.0 MANTENAY-MONTLIN 1 230 0 1.0 Maison 100.0 1.0 2103.0
... ... ... ... ... ... ... ... ... ... ... ... ... ...
3618967 2019-05-12 Vente 17521000.0 75004.0 PARIS 04 75 104 0 2.0 Appartement 47.0 1.0 470.0
3618968 2019-05-12 Vente 17521000.0 75004.0 PARIS 04 75 104 0 2.0 Appartement 52.0 2.0 470.0
3618969 2019-05-12 Vente 17521000.0 75004.0 PARIS 04 75 104 0 2.0 Appartement 100.0 4.0 470.0
3618970 2019-05-12 Vente 17521000.0 75004.0 PARIS 04 75 104 0 3.0 Dépendance 0.0 0.0 470.0
3618971 2019-05-12 Vente 17521000.0 75004.0 PARIS 04 75 104 0 2.0 Appartement 147.0 4.0 470.0

1007562 rows × 13 columns

Argent total dépensé par mois selon les types de mutation pendant l'année¶

In [92]:
MUTATIONS = fullData['Nature mutation'].unique()
def plotMutations(mut, data, ax):

    for m in MUTATIONS:
        temp = data[data['Nature mutation'] == m]
        result = temp.groupby(temp['Date mutation'].dt.to_period("M"))['Valeur fonciere'].sum()
        result.index = result.index.to_timestamp()
        x = result.index
        y = result.values
        
        if m == mut:
            ax.plot(x, y, color="#0b53c1", lw=2.4, zorder=10)
            ax.scatter(x, y, fc="w", ec="#0b53c1", s=60, lw=2.4, zorder=12)  
            ax.autoscale()    
        else:
            ax.plot(x, y, color="#BFBFBF", lw=1.5)
    
    ax.set_title(mut, fontfamily="Inconsolata", fontsize=14, fontweight=500)
    return ax
In [93]:
fig, axes = plt.subplots(2, 3, figsize=(14, 7.5))
for idx, (ax, mut) in enumerate(zip(axes.ravel(), MUTATIONS)):
    # Only annotate the first panel
    annotate = idx == 0
    plotMutations(mut, fullData, ax)

On remarque que la plupart des mutations au cours de l'année sont des ventes

In [94]:
data1 = fullData[fullData['Nature mutation'] =='Vente'] 
data1 = data1.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data1.index = data1.index.to_timestamp()

data2 = fullData[fullData['Nature mutation'] =='Vente terrain à bâtir'] 
data2 = data2.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data2.index = data2.index.to_timestamp()

data3 = fullData[fullData['Nature mutation'] =='Echange'] 
data3 = data3.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data3.index = data3.index.to_timestamp()

data4 = fullData[fullData['Nature mutation'] =="Vente en l'état futur d'achèvement"] 
data4 = data4.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data4.index = data4.index.to_timestamp()

data5 = fullData[fullData['Nature mutation'] =='Adjudication'] 
data5 = data5.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data5.index = data5.index.to_timestamp()

data6 = fullData[fullData['Nature mutation'] =='Expropriation'] 
data6 = data6.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data6.index = data6.index.to_timestamp()

plt.figure(figsize=(18,10))
plt.plot(data1.index, data1.values, "r--", color="red")
plt.plot(data2.index, data2.values, "r--", color="blue")
plt.plot(data3.index, data3.values, "r--", color="green")
plt.plot(data4.index, data4.values, "r--", color="yellow")
plt.plot(data5.index, data5.values, "r--", color="purple")
plt.plot(data6.index, data6.values, "r--", color="black")
plt.legend(['Vente','Vente terrain à bâtir', 'Echange',"Vente en l'état futur d'achèvement",'Adjudication','Expropriation'])
plt.title('Nombre de mutations par type au cours des mois, en cumulé')
plt.show()

On peut encore une fois confirmer que le seul type de mutation importante est la vente.

Nombre et répartitions des types de locaux¶

In [95]:
data = fullData.groupby(['Type local'])['Type local'].count()


plt.bar(data.index, data.values)
bars = ['Appartement', 'Dépendance', 'Industriel', 'Maison']
y_pos = np.arange(len(bars))
plt.xticks(y_pos, bars)
plt.title('Nombre de mutations par type de local')
Out[95]:
Text(0.5, 1.0, 'Nombre de mutations par type de local')
In [96]:
perc = [f'{i/data.values.sum()*100:5.2f}%' for i in data.values]
lbl = [f'{j[0]} = {j[1]}' for j in zip(data.index, perc)]

squarify.plot(sizes=data.values, label=lbl)
plt.axis('off')
plt.title('Proportion des types de locaux sur le nombre total de mutations')
plt.show()

On remarque que les mutations concernent principalement des maisons et des appartements.

In [97]:
data=fullData[fullData["Surface terrain"]< 5000]
plt.figure(figsize=(18,10))
plt.xticks(rotation=25)
sbn.violinplot(x = "Type local",y="Surface terrain", data=data)
plt.title('Répartition des types de locaux selon la surface de leur terrain')
Out[97]:
Text(0.5, 1.0, 'Répartition des types de locaux selon la surface de leur terrain')
In [98]:
data=fullData[(fullData["Surface reelle bati"]< 1000) & (fullData["Surface reelle bati"].notna())]
plt.figure(figsize=(18,10))
plt.xticks(rotation=25)
sbn.violinplot(x = "Type local",y="Surface reelle bati", data=data)
plt.title('Répartition des types de locaux selon leur surface réelle bâtie')
Out[98]:
Text(0.5, 1.0, 'Répartition des types de locaux selon leur surface réelle bâtie')

On voit ici que les données des dépendances sont assez peu intéressantes, puisque leur surface réelle bâtie est proche de zéro.

In [99]:
data = fullData[fullData['Valeur fonciere'] < 2000000]
plt.figure(figsize=(18,10))
plt.xticks(rotation=25)
sbn.violinplot(x="Type local",y="Valeur fonciere",data=data)
plt.title('Répartition des types de locaux selon leur valeur foncière')
Out[99]:
Text(0.5, 1.0, 'Répartition des types de locaux selon leur valeur foncière')

Analyse des données par département¶

In [100]:
data = fullData.groupby(['Code departement'])['Nature mutation'].count().sort_values(ascending=True)
plt.figure(figsize=(10,20))

plt.hlines(y=data.index, xmin=0, xmax=data.values, color='purple')
plt.plot(data.values, data.index, "o", color="gold")
 
# Add titles and axis names
#plt.yticks(data.index, data.index)
plt.title('Nombre de mutations par département')
plt.xlabel('Nombre de mutations')
plt.ylabel('Numéros de départements')
#data.plot.barh()
plt.show()
In [101]:
data1 = fullData.groupby(['Code departement'])['Nature mutation'].count()

data = fullData[((fullData['Nature mutation']=='Vente') & ((fullData['Type local'] == 'Maison') | (fullData['Type local'] == 'Appartement')))]
data = fullData.groupby(['Code departement'])['Nature mutation'].count()
plt.figure(figsize=(10,20))

plt.hlines(y=data1.index, xmin = 0, xmax = data1.values, color='red')
plt.hlines(y=data.index, xmin=0, xmax=data.values, color='skyblue')
plt.plot(data.values, data.index, "o")
plt.plot(data1.values, data1.index, "x", color="white")
 
# Add titles and axis names
#plt.yticks(data.index, data.index)
plt.title("Nombre de mutations par département (ronds) et nombre de mutations par département en considérant uniquement les ventes d'appartements et de maisons (croix)")
plt.xlabel('Nombre de mutations')
plt.ylabel('Numéros de départements')
#data.plot.barh()
plt.show()

On ne fait ici que confirmer visuellement que les ventes d'appartement et de maisons constituent la majorité écrasante de mutations, avec une variation extrêmement faible pour certains départements.

In [102]:
myscale = None

def mapping_france_folium(data):
    map = folium.Map(location=[48.862, 2.346], zoom_start = 5)
    departments = f"https://france-geojson.gregoiredavid.fr/repo/departements.geojson"
    d = {'Code': data.index, 'Valeur': np.log(data.values)}
    da = pd.DataFrame(d)

    folium.Choropleth(geo_data=departments, 
    data=da, 
    columns=['Code', 'Valeur'], 
    key_on='properties.code',
    fill_color= "PuRd",
    fill_opacity=1,
    line_opacity=.1).add_to(map)
    
    folium.LayerControl().add_to(map)
    return map


def mapping_Paris_circle(data, bigNumbers = False):
    map = folium.Map(location = [48.856578, 2.351828], zoom_start = 12)
    arr = json.load(open("arrondissements.geojson"))
    d = {'Code': data.index, 'Valeur': data.values}
    da = pd.DataFrame(d)
    for a in arr["features"]:
        prop = a["properties"]
        temp = da[da['Code'] == prop["c_arinsee"] - 100]
        temp = temp['Valeur'].values
        folium.Circle(prop["geom_x_y"], 
        fill=True,
        popup = prop["l_ar"],
        radius = (temp[0]/1) if not bigNumbers else temp[0]/9000000).add_to(map)
    return map


def mapping_Paris(data):
    map = folium.Map(location = [48.856578, 2.351828], zoom_start = 12)
    arr = json.load(open("arrondissements.geojson"))
    d = {'Code': data.index + 100, 'Valeur': np.log(data.values)}
    da = pd.DataFrame(d)
    da = da[(da['Code'] >= 75100) & (da['Code'] <= 75120)]
    myscale = np.linspace(da['Valeur'].min(), da['Valeur'].max(), 10)
    folium.Choropleth(geo_data=arr, 
            data=da, 
            columns=['Code', 'Valeur'], 
            key_on='properties.c_arinsee',
            fill_color= "PuRd",
            threshold_scale=myscale,
            fill_opacity=0.8,
            line_opacity=.1).add_to(map)
        
    
    folium.LayerControl().add_to(map)
    return map

def mapping_Lyon(data):
    map = folium.Map(location = [45.763420, 4.834277], zoom_start = 12)
    arr = json.load(open("adr_voie_lieu.json"))
    d = {'Code': data.index + 380, 'Valeur': np.log(data.values)}
    da = pd.DataFrame(d)
    da = da[(da['Code'] >= 69381) & (da['Code'] <= 69389)]
    folium.Choropleth(geo_data=arr, 
            data=da, 
            columns=['Code', 'Valeur'], 
            key_on='properties.insee',
            fill_color= "PuRd",
            threshold_scale=myscale,
            fill_opacity=0.8,
            line_opacity=.1).add_to(map)
    
    folium.LayerControl().add_to(map)
    return map

def mapping_Marseille(data):
    map = folium.Map(location = [43.296482, 5.36978], zoom_start = 12)
    arr = json.load(open("quartiers-marseille.geojson"))
    d = {'Code': data.index + 200, 'Valeur': np.log(data.values)}
    da = pd.DataFrame(d)
    da = da[(da['Code'] >= 13201) & (da['Code'] <= 13216)]
    da['Code'] = da['Code'].astype(int).astype(str)
    folium.Choropleth(geo_data=arr, 
            data=da, 
            columns=['Code', 'Valeur'], 
            key_on='properties.DEPCO',
            fill_color= "PuRd",
            threshold_scale=myscale,
            fill_opacity=0.8,
            line_opacity=.1).add_to(map)
    
    folium.LayerControl().add_to(map)
    return map
In [103]:
data = fullData.groupby(['Code departement'])['Nature mutation'].count()
map = mapping_france_folium(data)
map
Out[103]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ici, en échelle logarithmique, le nombre de mutations par département au cours de l'année.

In [104]:
data = fullData[fullData['Nature mutation'] == 'Vente'].groupby(['Code departement'])['Valeur fonciere'].sum()
map = mapping_france_folium(data)
map
Out[104]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus, en échelle logarithmique, la valeur cumulée des ventes par département au cours de l'année.

In [105]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data[data['Type local'] == 'Maison'].groupby(['Code departement'])['Valeur fonciere'].sum()
map = mapping_france_folium(data)
map
Out[105]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus, en échelle logarithmique, la valeur cumulée des ventes de maisons par département au cours de l'année. Il est intéressant de noter que l'importance de Paris dans la carte précédente disparaît : très peu de maisons sont vendues à Paris même.

In [106]:
data = fullData[(fullData['Nature mutation'] == 'Vente') & ((fullData['Type local'] == 'Maison') | (fullData['Type local'] == 'Appartement'))]
data['prix_m2'] = data['Valeur fonciere']/data['Surface reelle bati']
data = data.groupby(['Code departement'])['prix_m2'].mean()
#data = data.to_frame()
map = mapping_france_folium(data)
map
Out[106]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus, en échelle logarithmique, le prix au m2 par département. On remarque une certaine corrélation entre le nombre de ventes par département et le prix au m2, qu'on cherchera à confirmer par la suite

In [107]:
data = fullData[['Surface reelle bati','Valeur fonciere']]
plt.figure(figsize=(18,10))
plt.scatter(data['Surface reelle bati'],data['Valeur fonciere'])
plt.title('Répartition de la valeur foncière en fonction de la surface bâtie')
plt.show()

On cherche ici à montrer l'existence de valeurs extrêmes dans les données, qui nous ont forcé à adopter une échelle logarithmique pour les cartes, sans quoi nous aurions dû filtrer ces valeurs extrême (ci-dessous).

In [108]:
data = fullData[['Surface reelle bati','Valeur fonciere']]
data = data[data['Valeur fonciere'] < 1000000]
data = data[data['Surface reelle bati'] < 2000]
plt.figure(figsize=(18,10))
plt.scatter(data['Surface reelle bati'],data['Valeur fonciere'])
plt.title('Répartition de la valeur foncière en fonction de la surface bâtie, sans les valeurs extrêmes')
plt.show()

Analyse plus localisée sur Paris¶

Nombre de mutations par arrondissement¶

In [109]:
data = fullData.groupby(['Code postal'])['Nature mutation'].count()
map = mapping_Paris_circle(data, False)
map
Out[109]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus le nombre de mutations par arrondissement de Paris. Ci-dessous, on représente les mêmes données sur échelle logarithmique.

In [110]:
data = fullData.groupby(['Code postal'])['Nature mutation'].count()
map = mapping_Paris(data)
map
Out[110]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Valeur des ventes par arrondissement¶

In [111]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Paris_circle(data, True)
map
Out[111]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus la valeur des ventes par arrondissement de Paris. Ci-dessous, on représente les mêmes données sur échelle logarithmique.

On va s'intéresser par la suite à la valeur des ventes par arrondissement entre Paris, Lyon et Marseille. On pourra observer que la localisation des quartiers les plus recherchés est bien différente entre ces grandes villes.

In [112]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Paris(data)
map
Out[112]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [113]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Marseille(data)
map
Out[113]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [114]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Lyon(data)
map
Out[114]:
Make this Notebook Trusted to load map: File -> Trust Notebook

¶

In [115]:
data = fullData[['Valeur fonciere','Surface reelle bati','Surface terrain', 'Nombre pieces principales']]

sbn.heatmap(data.corr(), annot= True, cmap='Reds')
plt.xticks(rotation = 45)
plt.title('Corrélation entre la valeur foncière, la surface réelle bâtie, \nla surface du terrain et le nombre de pièces principales\n(en France))')
plt.show()
In [116]:
data = fullData[fullData['Code departement'] == '75']
data = data[['Valeur fonciere','Surface reelle bati','Surface terrain', 'Nombre pieces principales']]
sbn.heatmap(data.corr(), annot= True, cmap='Reds')
plt.xticks(rotation = 45)
plt.title('Corrélation entre la valeur foncière, la surface réelle bâtie, \nla surface du terrain et le nombre de pièces principales\n(en IDF))')
plt.show()

On remarquera une grande différence entre les données de corrélation en France, et en Île de France. Particulièrement entre la valeur foncière, la surface réelle bâtie, et la surface du terrain.